﻿using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.Threading.Tasks;
using XchangeStreamer.Utilities;

namespace XchangeStreamer.Publisher.SDK
{
    public sealed class Connector : IDisposable
    {
        #region Private Fields
        
        private static Connector connection;
        private Publisher publisher;
        private Sender sender;
        private EndpointAddress publisherAddress;
        private EndpointAddress senderAddress;
        private AsyncCallback onPublishCompleted;
        private AsyncCallback onSendCompleted;
        private int messageId = 0;
        private bool publisherOpened = false;
        private bool senderOpened = false;
        
        #endregion

        #region Private Const. / Dest.

        private Connector()
        {
            this.onPublishCompleted = new AsyncCallback(PublishCompleted);
            this.onSendCompleted = new AsyncCallback(SendCompleted);
        }

        ~Connector()
        {
            this.Disconnect(false);
        }

        #endregion

        #region Public Properties

        public static Connector Connection
        {
            get
            {
                if (connection == null)
                {
                    lock (typeof(Connector))
                    {
                        if (connection == null)
                        {
                            connection = new Connector();
                        }
                    }
                }
                return connection;
            }
        }

        #endregion

        #region Public Methods

        public void Connect(string streamerIP, string streamerPort, string cacheServerIP, string cacheServerPort)
        {
            this.Init(streamerIP, streamerPort, cacheServerIP, cacheServerPort);
            this.publisher = new Publisher(string.IsNullOrEmpty(streamerPort) == true ? Settings.NamedPipeBinding : Settings.TcpBinding, this.publisherAddress);
            this.sender = new Sender(string.IsNullOrEmpty(cacheServerPort) == true ? Settings.NamedPipeBinding : Settings.TcpBinding, this.senderAddress);
            this.Start();
        }

        public void Disconnect()
        {
            this.Disconnect(true);
        }

        public int Publish<T, K>(int topicId, K content)
        {
            var time = DateTime.UtcNow.TimeOfDay;
            var msgId = System.Threading.Interlocked.Increment(ref this.messageId);
            byte[] body;
            var isCompressed = Compression<T>.Compress<K>(content, out body);
            //xchangeMessage instances must be saved to a pool so we can use them as needed and avoid GC.
            var xchangeMessage = new XchangeStreamer.Core.Message { MessageId = msgId, TopicId = topicId, Time = time, Content = body, IsCompressed = isCompressed };
            var message = XchangeStreamer.Core.MessageConverter<XchangeStreamer.Core.Message>.ToMessage(xchangeMessage);
            //body = null;
            xchangeMessage = null;
            message.Headers.MessageId = new System.Xml.UniqueId(msgId.ToString());
            
            MessageBuffer messageBuffer = message.CreateBufferedCopy(65536);
            message.Close();

            this.publisher.BeginPublish(messageBuffer.CreateMessage(), null, null);
            this.sender.BeginSend(messageBuffer.CreateMessage(), null, null);

            messageBuffer.Close();
            return body.Length;
        }

        public int CreateTopic(string topicName)
        {
            return this.publisher.CreateTopic(topicName);
        }

        public void DeleteTopic(int topicId)
        {
            this.publisher.DeleteTopic(topicId);
        }

        public void UpdateTopic(int topicId, string topicName)
        {
            this.publisher.UpdateTopic(topicId, topicName);
        }

        #endregion

        #region Private Methods

        private void Init(string streamerIP, string streamerPort, string cacheServerIP, string cacheServerPort)
        {
            this.publisherAddress = string.IsNullOrEmpty(streamerPort) == true ? new EndpointAddress("net.pipe://" + streamerIP + "/Streamer/Publisher") :
                new EndpointAddress("net.tcp://" + streamerIP + ":" + streamerPort + "/Streamer/Publisher");

            this.senderAddress = string.IsNullOrEmpty(cacheServerPort) == true ? new EndpointAddress("net.pipe://" + cacheServerIP + "/CacheServer/Sender") :
                new EndpointAddress("net.tcp://" + cacheServerIP + ":" + cacheServerPort + "/CacheServer/Sender");
        }

        private void Start()
        {
            this.publisher.InnerChannel.Opening -= new EventHandler(this.Publisher_Opening);
            this.publisher.InnerChannel.Opened -= new EventHandler(this.Publisher_Opened);
            this.publisher.InnerChannel.Faulted -= new EventHandler(this.Publisher_Faulted);

            this.sender.InnerChannel.Opening -= new EventHandler(this.Sender_Opening);
            this.sender.InnerChannel.Opened -= new EventHandler(this.Sender_Opened);
            this.sender.InnerChannel.Faulted -= new EventHandler(this.Sender_Faulted);

            this.publisher.InnerChannel.Opening += new EventHandler(this.Publisher_Opening);
            this.publisher.InnerChannel.Opened += new EventHandler(this.Publisher_Opened);
            this.publisher.InnerChannel.Faulted += new EventHandler(this.Publisher_Faulted);

            this.sender.InnerChannel.Opening += new EventHandler(this.Sender_Opening);
            this.sender.InnerChannel.Opened += new EventHandler(this.Sender_Opened);
            this.sender.InnerChannel.Faulted += new EventHandler(this.Sender_Faulted);

            var publisher = Task.Factory.StartNew(() =>
            {
                while (!this.publisherOpened)
                {
                    try
                    {
                        this.publisher.InnerChannel.Open();
                        this.publisherOpened = true;
                    }
                    catch (Exception ex) { Console.WriteLine(ex.Message); }
                }
            });

            var sender = Task.Factory.StartNew(() =>
            {
                while (!this.senderOpened)
                {
                    try
                    {
                        this.sender.InnerChannel.Open();
                        this.senderOpened = true;
                    }
                    catch (Exception ex) { Console.WriteLine(ex.Message); }
                }
            });

            Task.WaitAll(publisher, sender);
            Console.WriteLine("Connected.");
        }

        void Publisher_Opening(object sender, EventArgs e)
        {
            Console.WriteLine("Publisher: Opening.");
        }

        void Publisher_Opened(object sender, EventArgs e)
        {
            Console.WriteLine("Publisher: Opened.");
        }

        private void Publisher_Faulted(object sender, EventArgs e)
        {
            Console.WriteLine("Publisher: Faulted");
            if (this.publisherOpened)
            {
                this.publisherOpened = false;
                this.Start();
            }
        }

        void Sender_Opening(object sender, EventArgs e)
        {
            Console.WriteLine("Sender: Opening.");
        }

        void Sender_Opened(object sender, EventArgs e)
        {
            Console.WriteLine("Sender: Opened.");
        }

        private void Sender_Faulted(object sender, EventArgs e)
        {
            Console.WriteLine("Sender: Faulted");
            if (this.senderOpened)
            {
                this.senderOpened = false;
                this.Start();
            }
        }

        private void Disconnect(bool suppressFinalize)
        {
            if (this.publisher != null)
            {
                this.publisher.Close();
                this.publisher = null;
            }

            if (this.sender != null)
            {
                this.sender.Close();
                this.sender = null;
            }

            if (suppressFinalize)
            {
                GC.SuppressFinalize(this);
            }
        }

        private void PublishCompleted(IAsyncResult result)
        {
            //((Publisher)result.AsyncState).EndPublish(result);
            //((System.ServiceModel.Channels.Message)result.AsyncState).Close();
        }

        private void SendCompleted(IAsyncResult result)
        {
            //((Sender)result.AsyncState).EndSend(result);
            //((System.ServiceModel.Channels.Message)result.AsyncState).Close();
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            this.Disconnect(true);
        }

        #endregion
    }
}